Repository navigation
Quote Select, join and GROUP BY column names as identifiers - #241
Open
simon-mundy wants to merge 6 commits into
Open
simon-mundy wants to merge 6 commits into
simon-mundy wants to merge 6 commits into
Conversation
Select resolved each column string through quoteIdentifierInFragment(), which splits on every non-word character, so a column such as x-y rendered as "x""-""y" while its alias rendered as "x-y". Column strings in the select list, join column lists and GROUP BY are now quoted as an identifier chain split only on the table-qualifier dot, with a * part left bare. A string holding an inline " AS " alias keeps fragment quoting, as before.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
A column string now switches to identifier-chain quoting only when fragment quoting would render it broken: a piece between the safe delimiters (space, "." and "*") that the platform would split and quote in parts, as with x-y, a"b or COUNT(id), or two names separated by spaces alone, as with "my col". Strings such as "price * qty" or "a*b" render exactly as before.
This was referenced Oct 13, 2026
Replace Select's own column-quoting helpers with IdentifierQuoter, which the ORDER BY and predicate fixes can share: - A plain name of word characters and qualifier dots takes a fast path straight to fragment quoting, as before. - Otherwise IdentifierShape reads the name in pieces. Fragment quoting is kept where it renders as written; a name it would split (x-y, my col, t.as) is quoted whole per qualifier part, with each part trimmed. - Expression syntax that no identifier quoting renders as written, such as COUNT(id), a - b or x-y * 2, throws InvalidArgumentException, as does an empty name or qualifier part. - An inline column alias such as 'x-y as z' renders "x-y" AS "z". TrustingHyphenWordPlatform becomes the shared hyphen-as-word test platform.
simon-mundy
added a commit
to simon-mundy/phpdb
that referenced
this pull request
Nov 4, 2026
simon-mundy
added a commit
to simon-mundy/phpdb
that referenced
this pull request
Nov 4, 2026
# Conflicts: # src/Sql/Select.php
The test restricts coverage to the factories it lists, so the new forEmptyIdentifierPart() and forUnquotableIdentifier() counted as uncovered and failed codecov/patch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Select fragment-quotes column names that are not plain or dotted words. On the SQL-92 platform:
x-y"foo"."x""-""y" AS "x-y""foo"."x-y" AS "x-y"my col"foo"."my" "col" AS "my col""foo"."my col" AS "my col"a"b"foo"."a""'""b" AS "a'b""foo"."a'b" AS "a'b"The alias was already quoted as a whole name, so the column and its alias disagreed. Join column lists and
GROUP BYrender the same broken way.Cause
processSelect()(select and join columns) andprocessGroup()resolve each column string throughAbstractSql::resolveColumnValue()withisIdentifier. That ends inAbstractPlatform::quoteIdentifierInFragment(), which splits the string on every character outside the platform's word pattern and quotes each piece. Only*, space,.andasare left bare (SAFE_WORDS). ORDER BY names (#243), predicate names (#244) and Update SET columns (#238) have the same cause.Fix: one shared
IdentifierQuoterThis PR adds
PhpDb\Sql\IdentifierQuoter(@internal), which #243 and #244 reuse, so every identifier slot follows one rule. Select overridesresolveColumnValue()and passes an identifier column string toIdentifierQuoter::quoteColumn()..*(id,t.a,t.*), goes straight toquoteIdentifierInFragment(). That renders it exactly as before and skips the rest of the check, so the common case stays cheap.IdentifierShapereads the name in pieces between spaces, dots and*, andIdentifierPiececlassifies each piece by asking the platform itself:price*qty,a as b,t.a * t.b.x-y,my col,café,t.as) is quoted whole. It is split only on., each part is trimmed and passed throughquoteIdentifier(), a*part stays bare, and the parts are joined withgetIdentifierSeparator().InvalidArgumentException::forUnquotableIdentifier(), pointing toExpression. This covers a bare operator (a - b), a function call (COUNT(id)), and a whole-quoted name mixed with*orAS(x-y * 2).'',a..b,t.) throwsInvalidArgumentException::forEmptyIdentifierPart().x-y as zrenders as"x-y" AS "z", with each side quoted separately.The pieces are classified with the platform's own
quoteIdentifierInFragment()andquoteIdentifier(), so each platform's fragment pattern is respected. phpdb-mysql treats-as a word character, so`x-y`and`a-b` * `c`render as before there. WithquoteIdentifiersoff, both methods return the string as given, so nothing changes.Expressions, sub-selects and every other use of
resolveColumnValue()(Update, Insert) still go through the parent method unchanged.What stays the same
docs/book, the unit tests, TableGateway/RowGateway and phpdb-mysql, phpdb-sqlite and phpdb-pgsql only pass plain names, dotted names,
*and aliased keys as column strings. SQL always goes throughExpression. All of these render byte-for-byte as before, as does every string that renders valid SQL today:t.*,price * qty,a*b,t.price * t.qty, and the inline aliasbar AS baz.Behaviour change
Output changes only for strings that rendered invalid SQL before:
COUNT(id),x-y * 2), and an empty name or qualifier part, now throwInvalidArgumentException. They used to render broken SQL, which only failed at the database.Performance
Measured locally with
php -n, rendering a 15-identifier Select, best of 5 runs:The previous per-PR helpers cost +35% here, and +58% combined. The fast path accounts for most of the difference.
Tests
test/unit/Sql/IdentifierQuoterTest.php(#[Group('unit')]): 48 data-provider cases.t.as;TrustingHyphenWordPlatformasset), identifier quoting off, and the platform's identifier separator;test/unit/Sql/SelectColumnNameTest.php: select columns, join columns and GROUP BY, including rejected expressions in each.Checks:
IdentifierQuoter,IdentifierShapeandIdentifierPiece.mago format --check,mago lintandmago analyzeexit 0 on PHP 8.3, 8.4 and 8.5, and so doesmago guard.Follow-ups (not in this PR)
'my col DESC'parses as the namemywith an unknown direction ("my" ASC). This is a parsing problem, not a quoting one.t.*in the select or join list still getsAS "t.*"appended.ASin join columns: the join list appends a second alias ("zac"."bar" AS "baz" AS "bar AS baz").'forquoteIdentifierTo, so an embedded"becomes'rather than"".